//
//  MySQLResult.m
//  LibMySQL
//
//  Created by Marcus Grenängen on 2009-07-20.
//  Copyright 2009 SneWs Soft. All rights reserved.
//
#import "MySQLResult.h"

@interface MySQLResult (Private)
@property (nonatomic, readwrite, retain) NSMutableArray *columns;
@property (nonatomic, readwrite, retain) NSMutableDictionary *columnMappings;
@property (nonatomic, readwrite, assign) MYSQL *connection;
@property (nonatomic, readwrite, assign) MYSQL_RES *result;
@property (nonatomic, readwrite, assign) long numRows;
@property (nonatomic, readwrite, assign) long numColumns;

- (void)_parseColumnInfo;
- (MySQLRow *)_parseResultRow:(MYSQL_ROW)row;
@end

@implementation MySQLResult

@synthesize columns;
@synthesize columnMappings;
@synthesize connection;
@synthesize result;
@synthesize numRows;
@synthesize numColumns;

- (id)init {
	return [self initWithResult: NULL andConnection: NULL usingEncoding: NSISOLatin1StringEncoding];
	
} // END METHOD: init: 

- (id)initWithResult:(MYSQL_RES *)theResult andConnection:(MYSQL *)c usingEncoding:(NSStringEncoding)e {
	if( self = [super init] ) {
		stringEncoding = e;
		columns = [[NSMutableArray alloc] init];
		columnMappings = [[NSMutableDictionary alloc] init];
		
		connection = c;
		result = theResult;
		
		// Parse and save column info
		[self _parseColumnInfo];
		
		// We don't set this untill requested... (Lazy init)
		numRows = -1L;
		numColumns = -1L;
		
	} // END if (self = [super init])
	
	return self;
	
} // END METHOD: initWithResult: 

- (void)dealloc {
	[columns release];
	[columnMappings  release];
	
	if( result != NULL ) {
		mysql_free_result( result );
		result = NULL;
		
	} // END if (result != NULL)
	
	// The MySQLConnection class still has ownership and responsibillity
	// for the MYSQL connection object... Good or bad. 
	// Any better solution(s) for this approatch?
	
	[super dealloc];
	
} // END METHOD: dealloc: 

#pragma mark -
#pragma mark Public fetch operations
- (long)rowCount {
	if( numRows < 0 ) {
		if( result != NULL )
			numRows = mysql_num_rows( result );
		
	} // END if (numRows < 0)
	
	return self.numRows;
	
} // END METHOD: rowCount:

- (long)columnCount {
	if( numColumns < 0 ) {
		if( columns != nil )
			numColumns = [columns count];

	} // END if (numColumns < 0)
	
	return self.numColumns;
	
} // END METHOD: columnCount:


/*
 * @description: Returns the row at the specified index
 */
- (MySQLRow *)rowAtIndex:(NSUInteger)rowIndex {
	if( rowIndex < 0 || rowIndex > [self rowCount] )
		@throw [[NSException alloc] initWithName: @"Invalid Operation Exception" 
										  reason: @"The row index is not inside the allowed range." 
										userInfo: nil];
	
	mysql_data_seek( result, rowIndex );
	MYSQL_ROW	   row = mysql_fetch_row( result );
	
	return [self _parseResultRow: row];
}

#pragma mark -
#pragma mark Fast enumeration
- (NSArray*)allObjects {
	// We just fetch the rest of the available objects...
	NSMutableArray *retVal = [[NSMutableArray alloc] init];
	
	id obj;
	while( obj = [self nextObject] )
		[retVal addObject: obj];
	
	return [retVal autorelease];
	
} // END METHOD: allObjects: 

- (id)nextObject {
	MYSQL_ROW	   row = mysql_fetch_row( result );
	return [self _parseResultRow: row];
	
} // END METHOD: nextObject:

#pragma mark -
#pragma mark Private Methods
- (void)_parseColumnInfo {
	MYSQL_FIELD  *cols;
	unsigned int len;
	unsigned int i;
	MySQLColumn  *mc;
	
	if( result != NULL ) {
		cols = mysql_fetch_fields( result );
		len = mysql_field_count( connection );
		
		for (i = 0; i < len; ++i) {
			mc = [[MySQLColumn alloc] initWithName:[NSString stringWithUTF8String:cols[i].name]
										  position:i
									 mySQLDataType:cols[i].type];
			
			[columns addObject: [mc autorelease]];
			[columnMappings setValue:[NSNumber numberWithInt:i] forKey:[mc name]];
	
		} // END for (i = 0; i < len; ++i)
		
	} // END if (result != NULL)
	
} // END METHOD: _parseColumnInfo: 

- (MySQLRow *)_parseResultRow:(MYSQL_ROW)row {
	NSMutableArray *values;
	MySQLRow	   *newRow = nil;
	int			   i;
	
	if( row != NULL ) {
		values = [NSMutableArray arrayWithCapacity: [columns count]];
		
		for( i = 0; i < [columns count]; ++i ) {
			[values addObject: [MySQLResult convertToObjCValue: row[i]
												 fromMySQLType: ( (MySQLColumn *)[columns objectAtIndex: i] ).mysqlDataType
													 withFlags: 0
												 usingEncoding: stringEncoding]];
			
		} // END for (i = 0; i < [columns count]; ++i)
		
		newRow = [[MySQLRow alloc] initWithData: columns values: values map: columnMappings];
		
	} // END if (row != NULL)
	
	return [newRow autorelease];	
	
} // END METHOD: _parseResultRow: 



- (NSString *)description {
	return [NSString stringWithFormat: @"MySQLResult\nNumber of rows: %i\nNumber of columns: %i",
			[self rowCount], [self columnCount]];

} // END METHOD: description: 



// Data convert helpers
+ (id)convertToObjCValue:(char*)mysqlValue fromMySQLType:(int)t withFlags:(int)flags usingEncoding:(NSStringEncoding)encoding {	
	if( mysqlValue == NULL )
		return [NSNull null];
	
	/// TODO: atoll() atof() etc is considered unsafe and should be replaced with
	/// strtod(), strtof(), strtold() instead. 
	/// ---------------------------
	/// See stdlib.h
	
	id retVal			= nil;
	NSString *value		= [NSString stringWithCString: mysqlValue encoding: encoding];
	
	switch( t ) {
		case MYSQL_TYPE_BIT:
			retVal = [NSNumber numberWithInt: [value intValue]];
			break;
		
		/* Binary data */
		case MYSQL_TYPE_TINY_BLOB:
		case MYSQL_TYPE_MEDIUM_BLOB:
		case MYSQL_TYPE_BLOB:
		case MYSQL_TYPE_LONG_BLOB:
			if( ( flags & BINARY_FLAG ) > 0 )
				retVal = [value dataUsingEncoding: encoding];
			else
				retVal = value;
			break;
		
		/* Date and Time */
		case MYSQL_TYPE_DATE:
		case MYSQL_TYPE_DATETIME:
		case MYSQL_TYPE_NEWDATE:
		case MYSQL_TYPE_TIMESTAMP:
			if( t != MYSQL_TYPE_TIMESTAMP )
				// Please note, dateWithString will not work here...
				// tring representation format—YYYY-MM-DD HH:MM:SS ±HHMM, where ±HHMM is a time zone offset in hours and minutes from GMT
				// You must specify all fields of the format, MySQL does not do this by default...
				retVal = [NSDate dateWithNaturalLanguageString: value];
			else
				retVal = [NSDate dateWithTimeIntervalSince1970: [value integerValue]];
			break;
		
		/* Decimal numeric values */
		case MYSQL_TYPE_DOUBLE:
			retVal = [NSNumber numberWithFloat: [value doubleValue]];
			break;
			
		case MYSQL_TYPE_DECIMAL:
		case MYSQL_TYPE_FLOAT:
		case MYSQL_TYPE_NEWDECIMAL:
			retVal = [NSNumber numberWithFloat: [value floatValue]];
			break;
		
		/* Numeric values */
		case MYSQL_TYPE_TINY:
		case MYSQL_TYPE_SHORT:
		case MYSQL_TYPE_INT24:
		case MYSQL_TYPE_YEAR:
			retVal = [NSNumber numberWithInt: [value intValue]];
			break;
		
		case MYSQL_TYPE_LONG:
			retVal = [NSNumber numberWithLong: (long)[value longLongValue]];
			break;
			
		case MYSQL_TYPE_LONGLONG:
			retVal = [NSNumber numberWithLongLong: [value longLongValue]];
			break;
			
		/* String & Enum values */
		case MYSQL_TYPE_STRING:
		case MYSQL_TYPE_VAR_STRING:
		case MYSQL_TYPE_VARCHAR:
		case MYSQL_TYPE_ENUM:
			retVal = value;
			break;
		
		/* NULL and undefined values from here. */
		case MYSQL_TYPE_NULL:
		default:
			retVal = [NSNull null];
			break;
			
	} // END switch( t )
	
	/// TODO: How will one handle this case (Best practice?)
	// This is most probable to be caused by strings not beeing parsed properly based on encoding from database to client.
	// NS* object. set it to NSNull for now.
	if( retVal == nil )
		retVal = [NSNull null];
	
	return retVal;
	
} // END METHOD: convertToObjCValue: 

@end